package mini;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import mini.compiler.CodeGenerator;
import mini.expr.Expr;
import mini.util.MiniUtil;
import mini.util.SymbolicNames;

public class Proc {
	
	// We keep a list of what number procedure this is, for the purpose of creating
	// unique symbolic labels to indicate its location in memory
	private static int procedureNumber = 0;

    private ParamList parameterlist;
    private StatementList stmtlist;
    
    // Slide references to: https://www.cs.drexel.edu/~jjohnson/2011-12/spring/cs550/lectures/compiler.pdf
    // These variables are from slide 28, which states we need them
    private int numberOfInstructions = 0;
    
    // This is the label for the location so that it can be used in JMP statements elsewhere
    private String locationLabel = null;
    
    // And I believe we need to store the code here with the function, so we can lay it out later
    private String theCode = null;

    // We keep a map of unique user and temp variables and parameters, along with unique
    // locations, so that we can record their offset in the activation
    // record. This will be needed at link time
	private Map<String,String> userVariables = new HashMap<String,String>();
	private Map<String,String> tempVariables = new HashMap<String,String>();
	private Map<String,String> parameters = new HashMap<String,String>();
	
	// Constants, we need to map to their actual value
	private Map<String,Integer> constants = new HashMap<String,Integer>();

    public Proc(ParamList pl, StatementList sl) {
    	setParameters(pl);
        stmtlist = sl;
    }
    
    /**
     * This is used only for creating the main function
     * @param code
     */
    protected Proc( Map<String,Integer> hm, String code ) {
    	
    	// We don't want the parameter list or statement lists to be null
    	parameterlist = new ParamList();
    	stmtlist = new StatementList();
    	
    	setTheCode( hm, code );
    }

    /**
     * Sets the parameters. This does two things:
     * 
     * 1. Sets this parameterlist object that we might need later
     * 2. Sets a mapping of parameters to symbolic names, so that
     *    people can access them later
     * @param list
     */
    public void setParameters( ParamList list ) {
    	
        parameterlist = list;
        
        if( list != null && list.getParamList() != null ) {
        	List<String> params = list.getParamList();
        	int offset = 0;
        	for( String param : params ) {
				parameters.put(param,getOffsetConstant(null,offset++));
        	}
        }
    }
        
    public ParamList getParameters(){
    	return parameterlist;
    }

    public int apply(HashMap<String, Integer> nametable, HashMap<String, Proc> functiontable, LinkedList var, ExpressionList expressionlist) {
        //System.out.println("Executing Proceedure");
        HashMap<String, Integer> newnametable = new HashMap<String, Integer>();

        // bind parameters in new name table
        // we need to get the underlying List structure that the ParamList uses...
        Iterator<String> p = parameterlist.getParamList().iterator();
        Iterator<Expr> e = expressionlist.getExpressions().iterator();

        if (parameterlist.getParamList().size() != expressionlist.getExpressions().size()) {
            System.out.println("Param count does not match");
            System.exit(1);
        }
        while (p.hasNext() && e.hasNext()) {

            // assign the evaluation of the expression to the parameter name.
            newnametable.put(p.next(), e.next().eval(nametable, functiontable, var));
        //System.out.println("Loading Nametable for procedure with: "+p+" = "+nametable.get(p));

        }
        // evaluate function body using new name table and 
        // old function table
        // eval statement list and catch return
        //System.out.println("Beginning Proceedure Execution..");
        try {
            stmtlist.eval(newnametable, functiontable, var);
        } catch (Exception result) {
            // Note, the result shold contain the proceedure's return value as a String
            //System.out.println("return value = "+result.getMessage());
            return Integer.parseInt(result.getMessage());
        }
        System.out.println("Error:  no return value");
        System.exit(1);
        // need this or the compiler will complain, but should never
        // reach this...
        return 0;
    }
    
    /**
     * The parse routine will:
     * 
     * <ol>
     * <li>Store code in theCode variable
     * <li>Compute the number of parameters, number of variables, and number of temp/local variables, then set them
     * <li>Add the procedure to the functiontable
     * </ol>
     * @param nametable
     * @param functiontable
     * @param var
     * @param expressionlist
     * @return
     */
    public String parse(HashMap<String, Integer> hm, HashMap<String, Proc> functiontable, LinkedList var ) {

        // All we have to do is set the code
        String theCode = stmtlist.parse(hm, functiontable, var);
        setTheCode( hm, theCode );
        
    	return "";
    }


    /**
     * When we get a bunch of lines of code, the labels can be on lines before their
     * associated statements. So it looks like this:
     * 
     * loc0:
     * LDA i
     * 
     * We need to change it so that they're on the same line:
     * 
     * loc0: LDA i
     * 
     * Also, sometimes a block of code ends with a location label, and no code following.
     * This should only be the case of unreachable code. Since we don't have a NOOP code
     * to use, just have it jump to itself. If it ever reaches that there was going to 
     * be problems anyway, with a lack of code. (This seems to happen when there's something
     * like this as a function:
     * 
     *   if n then return n + addr(n-1) else return 0 fi
     *
     * Since the returns are in the if/else structure, it never gets to code beyond that
     * if/then block. But just the way parsing is set up, since it doesn't KNOW there are
     * return statements, it'll try to stick a label afterward and jump to it.
     * 
     * @param code The code that has labels potentially on a line by itself. It also might
     * have a trailing label with no code.
     * @return New code, that has labels on their lines of code. Also, it has a jump statement
     * on a trailing label, just to make things syntactically correct.
     */
    private String collapseLabels(String originalCode) {
    	
    	// Split the original code into trimmed lines (and for some reason, using
    	// split sometimes gets empty lines, because there are spaces or something?)
    	String[] possiblyIncludesEmptyLines = originalCode.split( Program.NEWLINE );
    	
    	// When we pu them in the list, we'll guarantee no empty strings get copied over
    	List<String> lines = new ArrayList<String>();
    	for( String possiblyEmptyLine : possiblyIncludesEmptyLines ) {
    		if( possiblyEmptyLine.trim().length() > 0 ) {
    			lines.add( possiblyEmptyLine.trim() );
    		}
    	}
    	
    	StringBuffer newCode = new StringBuffer();
    	
    	// STEP 1: This is what ensures labels are on the same line as their code that follows
    	String lastLabel = null;
    	for( String line : lines ) {
    		newCode.append( line );
    		if( SymbolicNames.isLocationLabel(line) || SymbolicNames.isProcedureLabel(line) ) {
    			// Keep track of the last label in case we need it in step 2 below
    			lastLabel = line.substring(0,line.length()-1);
    			newCode.append( " " );
    		} else {
    			newCode.append( Program.NEWLINE );
    		}
    		
    	}
    	
    	// STEP 2: This is what guarantees that there is not a last line that is solely a label; 
    	// it needs code, even though that code will never execute, for syntactic correctness
    	if( newCode.toString().trim().endsWith(":") ) {
    		newCode.append( "JMP " );
    		newCode.append( lastLabel );
    		newCode.append( " ; This code should never be reached" );
    	}
    	
		return newCode.toString();
	}

	/**
     * Given some code, sort out the temporary variables, the
     * user variables, and the constants. (Later, the user
     * and temporary variables will be stored in the activation
     * record, while the constants will be in global memory.)
     * 
     * @param code Symbolic instructions
     */
	private void recordVariablesAndConstants(Map<String,Integer> hm, String code) {

		String[] lines = code.split(Program.NEWLINE);
		
		// Running list of user variables and temps, so I can allocate them contiguously in memory
		List<String> runningListOfTemps = new ArrayList<String>();
		List<String> runningListOfVars = new ArrayList<String>();
		
		// When we start giving offsets for temp and user variables,
		// we start with an offset that is 0 + number of parameters
		int offset = getNumberOfParameters();
		
		// If we store the variables in a set, since duplicates aren't
		// stored, at the end the length will be the number of them
		for( String line : lines ) {
			// Strip off any comments
			int semicolonAt = line.indexOf(";");
			if( semicolonAt != -1 ) {
				line = line.substring(0,semicolonAt);
			}
			
			// At this point, the /last/ token is the variable name (unless it's a HLT instruction!)
			String[] tokens = line.split(" ");
			String variableName = tokens[tokens.length-1].trim();
			String operator = null;
			// The one before, if anything, is the operator
			if( tokens.length >= 2 ) {
				operator = tokens[tokens.length-2];
			}
			
			// Might have a user variable, temp variable, location, or constant
			if( variableName.equals("HLT") ) {
				// This is hackish, but since we're picking the last non-comment 
				// word and calling it the variable, and since HLT has only 
				// world, we have to not claim it's a variable
			}
			else if( isUserVariable(variableName) ) {
				// HACK! If the operator was that meta-operator about storing the AR
				// size later, it's not a user variable, so don't try to reserve
				// space for it
				// HACK: If the operator was a jump-type function, we also don't care
				// about storing the user variable as it's a location it's jumping to
				if( operator != null && !operator.equals(CodeGenerator.ADD_SIZE_OF_ACTIVATION_RECORD)
						&& !CodeGenerator.isJumpInstruction(operator)) {
					// Only need one copy of them, but keep in a list for ordering
					if( !runningListOfVars.contains(variableName) ) {
						runningListOfVars.add(variableName);
					}
				}
			} else if( SymbolicNames.isTempVariable(variableName) ) {
				// Only need one copy of them, but keep in a list for ordering
				if( !runningListOfTemps.contains(variableName) ) {
					runningListOfTemps.add(variableName);
				}
			} else if( SymbolicNames.isConstant(variableName) ) {
				// Only need one copy of them, but keep in a list for ordering
				if( !constants.containsKey(variableName) ) {
					// Since this is a constant, we MUST find it in the symbol table
					if( !hm.containsKey(variableName) ) {
						System.err.println( "Could not find constant '" + variableName + "' in the symbol table of size " + 
									(hm == null ? "null" : hm.size() ) );
					} else {
						constants.put(variableName, hm.get(variableName) );
					}
				}
			}
		}
		
		// Cycle through vars first, then temps, to allocate space
		for( String var : runningListOfVars ) {
			userVariables.put(var,getOffsetConstant(hm,offset++));
		}
		for( String temp : runningListOfTemps ) {
			tempVariables.put(temp,getOffsetConstant(hm,offset++));
		}

	}

	/**
	 * Computes a constant label for a value in memory
	 * equivalent to the value. Ensure that this is
	 * defined as a constant in memory, so it can be
	 * allocated appropriately.
	 * 
	 * @param value Integer value for equivalence
	 * @return A string like "Constant1" or "Constant27"
	 * or whatever
	 */
    private String getOffsetConstant( Map<String,Integer> hm, int value ) {
    	// Figure out its name, by convention
    	String constantName = SymbolicNames.generateConstantLabel(value);
    	
    	// Put it in the symbol table
    	if( hm != null ) {
        	hm.put(constantName, value);
    	}
    	
    	return constantName;
	}

	/**
     * Something is a parameter name if it's stored in the
     * parameter list.
     * 
     * @param variableName Variable name
     * @return True if parameter name, false otherwise
     */
    private boolean isParameter(String variableName ) {
    	return  parameterlist.getParamList().contains(variableName); 	
    }
    
    /**
     * Something is a user variable if it's not a location,
     * and not a parameter, and not a constant, and not
     * a temp variable.
     * 
     * @param variableName Variable name
     * @return True if user variable, false otherwise
     */
    private boolean isUserVariable(String variableName ) {
    	return !SymbolicNames.isTempVariable(variableName) &&
    			!SymbolicNames.isConstant(variableName) &&
    			!SymbolicNames.isLocationLabel(variableName) &&
    			!isParameter(variableName) &&
    			!SymbolicNames.isProcedureLabel(variableName) &&
    			variableName != null;
    }

	/**
     * Given some code, count up the number of instructions in it,
     * and return.
     * 
     * @param code
     * @return Number of instructions
     */
	private int getNumberOfInstructions(String code) {
		String[] lines = code.split(Program.NEWLINE);
		return lines.length;
	}

	public int getNumberOfParameters() {
		if( parameterlist != null && parameterlist.getParamList() != null ) {
			return parameterlist.getParamList().size();
		}
		return 0; 
	}

	public int getNumberOfLocalTempVariables() {
		return userVariables.size() + tempVariables.size();
	}
	
	/**
	 * A unique list of temp variables
	 * @return temp variables
	 */
	public Set<String> getTempVariables() {
		return tempVariables.keySet();
	}

	/**
	 * A unique list of user variables
	 * @return user variables
	 */
	public Set<String> getUserVariables() {
		return userVariables.keySet();
	}

	public int getNumberOfInstructions() {
		return numberOfInstructions;
	}

	private void setNumberOfInstructions(int numberOfInstructions) {
		this.numberOfInstructions = numberOfInstructions;
	}

	public String getTheCode() {
		return theCode;
	}

	/**
	 * When we set the code, we actually have to do a few things before we set it:
	 * 
	 * <ol>
	 * <li>Give it a starting label, so it can be jumped to symbolically.
	 * <li>Collpase labels, so everything is formatted in a syntactically 
	 * correct fashion
	 * </ol>
	 * @param code
	 */
	public void setTheCode( Map<String,Integer> hm, String code) {
		
		// Here is where we handle giving it a label
		code = getLocationLabel() + ": " + code; 
		
		// Here is where we collapse the labels for syntactic correctness
		this.theCode = collapseLabels(code );
		
        // We allegedly must know the number of instructions
        this.setNumberOfInstructions( getNumberOfInstructions(theCode) );
        
        // We know the parameters from the parameterlist, but we also may want
        // to know an ordered list of variables, and of constants
        recordVariablesAndConstants( hm, theCode );        
	}
	
	/**
	 * The code for the procedure will be given this label, which
	 * can be used for jumps.
	 * 
	 * @return The label of the first instruction of the procedure
	 */
	public String getLocationLabel() {
		
		if( locationLabel == null ) {
			locationLabel = SymbolicNames.generateProcedureLabel( procedureNumber++ );
		}
		
		return locationLabel;
	}
	
	/**
	 * Given a user variable name, this will return a symbolic name for
	 * the value that has to be added to the FP to reference it, in an
	 * activation record.
	 * 
	 * For example, on Slide 32 the variable 'i' is at an offset of +1
	 * from the FP, on the activation record. This method would return
	 * a symbolic name of the number 1. In this example, using our
	 * conventions, it would probably be Constant1. Whatever it is, it's
	 * guaranteed that if you add it to the FP, you will end up at the
	 * memory location for the user variable in question.
	 * 
	 * @param variableName User variable of interest
	 * @return A symbolic name of an integer value that has to be added
	 * to the FP to access this variable's memory address in the
	 * activation record. If it can't find the variable it will return
	 * null (which is bad)
	 */
	public String findSymbolicOffsetForUserVariable( String variableName ) {
		return userVariables.get(variableName);
	}
	
	/**
	 * Given a temp variable name, this will return a symbolic name for
	 * the value that has to be added to the FP to reference it, in an
	 * activation record.
	 * 
	 * For example, on Slide 32 the variable 'T1' is at an offset of +3
	 * from the FP, on the activation record. This method would return
	 * a symbolic name of the number 3. In this example, using our
	 * conventions, it would probably be Constant3. Whatever it is, it's
	 * guaranteed that if you add it to the FP, you will end up at the
	 * memory location for the temp variable in question.
	 * 
	 * @param variableName Temp variable of interest
	 * @return A symbolic name of an integer value that has to be added
	 * to the FP to access this variable's memory address in the
	 * activation record. If it can't find the variable it will return
	 * null (which is bad).
	 */
	public String findSymbolicOffsetForTempVariable( String variableName ) {
		return tempVariables.get(variableName);
	}

	/**
	 * This is similar to {@link #findSymbolicOffsetForTempVariable(String)}
	 * and {@link #findSymbolicOffsetForUserVariable(String)}, only
	 * you don't have to know which of the two it is.
	 * 
	 * @param variableName Temp variable of interest
	 * @return A symbolic name of an integer value that has to be added
	 * to the FP to access this variable's memory address in the
	 * activation record. If it can't find the variable it will return
	 * null (which is bad).
	 */
	public String findSymbolicOffsetForVariable( String variableName ) {
		String tempLocation = tempVariables.get(variableName);
		if( tempLocation != null ) {
			return tempLocation;
		}
		tempLocation = userVariables.get(variableName);
		if( tempLocation != null ) {
			return tempLocation;
		}
		return parameters.get(variableName);
	}

	/**
	 * This is a map of strings that are constants, to integers
	 * that are their values
	 * 
	 * @return The mapping
	 */
	public Map<String, Integer> getConstants() {
		return constants;
	}

	/**
	 * Returns the size of the activation record that would need to be
	 * allocated for this function. It's equal to the number of parameters,
	 * plus the number of user/temp variables, plus 3 (for the prev FP,
	 * return value, and return address
	 * 
	 * @return
	 */
	public Integer getSizeOfAR() {
		return this.getNumberOfLocalTempVariables() + this.getNumberOfParameters() + 3;
	}

}
